Enhancement: Public extension manager for ECH#10568
Enhancement: Public extension manager for ECH#10568sebastian-carpenter wants to merge 5 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
This PR refactors TLS 1.3 ECH public-vs-private extension handling by introducing a dedicated “public extensions” list on the WOLFSSL_ECH object and swapping extensions by type, replacing the previous SNI-only in-place string swap approach. It also updates SNI/ECH parsing and handshake flow to install the correct extensions on accept/reject paths, and adds/updates tests to validate wire visibility of public vs private SNI.
Changes:
- Add
WOLFSSL_ECH::extensionsto hold “public” extensions and implement swapping/replacement helpers used during ClientHello size/write and accept/reject handling. - Rework SNI parsing and ECH handshake transitions to remove the old
sniState/privateNamemachinery and support public-name matching for outer ClientHello. - Update API tests to cover new behaviors (wire-SNI visibility, no-inner-SNI behavior, new bad config cases) and relocate long-SNI length validation coverage.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
wolfssl/internal.h |
Removes old SNI-state fields and adds WOLFSSL_ECH::extensions plus TLSX_EchReplaceExtensions() prototype. |
src/tls.c |
Implements public-extension swapping/replacement, updates SNI parsing, and updates ECH lifecycle management. |
src/tls13.c |
Adjusts ClientHello padding derivation and installs correct extension sets at ECH accept/reject points (incl. HRR). |
tests/api.c |
Adds/updates ECH/SNI tests (wire visibility, no-private-name behavior, new config cases) and relocates length checks. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
2e125d1 to
aa4831f
Compare
|
retest this please |
|
8c2fa10 to
61e1638
Compare
|
Jenkins retest this please. |
dgarske
left a comment
There was a problem hiding this comment.
Skoll Code Review
Scan type: reviewOverall recommendation: COMMENT
Findings: 4 total — 4 posted, 0 skipped
4 finding(s) posted as inline comments (see file-level comments below)
Posted findings
- [Medium] Multi-public-extension swap/reversal path is untested —
src/tls.c:16682-16734 - [Low] ECH padding length only considers ssl-extensions, not ctx-level inner SNI —
src/tls13.c:4844-4855 - [Info] Pointer declaration style inconsistent with repo convention —
src/tls.c:2378 - [Info] TLSX_EchShouldHideInner can be a single boolean expression —
src/tls.c:16665-16671
Review generated by Skoll
| * popCount extensions are 'reversed' off the list. | ||
| * | ||
| * Returns a count of extensions prepended to sslExts. */ | ||
| static word16 TLSX_EchSwapExtensions(TLSX** sslExts, TLSX** echExts, |
There was a problem hiding this comment.
🟠 [Medium] Multi-public-extension swap/reversal path is untested
TLSX_EchSwapExtensions contains non-trivial pointer manipulation specifically designed for multiple public extensions: the popCount head-pop/reverse loop and the prepend-and-reverse ordering logic exist to preserve OuterExtensions ordering when more than one public extension is present. The PR body explicitly states this lays "groundwork for managing multiple public extensions and multiple SNI's". However, in the current code ech->extensions only ever holds a single SNI (populated in TLSX_ECH_Use and TLSX_SNI_Parse), so the multi-node popCount > 1 reversal path and the multi-prepend ordering are never exercised by any test. The new test_wolfSSL_Tls13_ECH_wire_sni only validates the single-SNI case. Since this is the most intricate part of the change, a regression here would be silent until the future multi-ext work lands.
Fix: Add a small unit test for the multi-extension swap/reverse ordering to lock in the reversal behavior the PR is building toward.
There was a problem hiding this comment.
Added testing (more than a little...)
| /* calculate padding (RFC 9849, section 6.1.3) */ | ||
| if (args->ech->privateName != NULL) { | ||
| word16 nameLen = (word16)XSTRLEN(args->ech->privateName); | ||
| nameLen = TLSX_SNI_GetRequest(ssl->extensions, |
There was a problem hiding this comment.
🔵 [Low] ECH padding length only considers ssl-extensions, not ctx-level inner SNI
The replacement padding calculation reads the inner SNI via TLSX_SNI_GetRequest(ssl->extensions, ...). This only inspects ssl->extensions, whereas a client may configure its SNI at the CTX level via wolfSSL_CTX_UseSNI (which lands in ssl->ctx->extensions). In that configuration hostName will be NULL here and the code takes the paddingLen = maxNameLen + 9 branch as if no inner name were present. The effect is benign (more padding, never a buffer issue, and the actual outer SNI on the wire is still correct due to the semaphore override in TLSX_GetSize/TLSX_Write), but the padding no longer reflects the true inner name length, slightly weakening the length-hiding intent of RFC 9849 6.1.3 for ctx-level-SNI clients. The old code used ech->privateName. Worth confirming this is intentional / acceptable.
Fix: Confirm ctx-level inner SNI is out of scope, or add a ctx->extensions fallback for the padding length calculation.
There was a problem hiding this comment.
Updated. Now checks the ctx if SNI is not found on the ssl
| word16 offset = 0; | ||
| int cacheOnly = 0; | ||
| SNI *sni = NULL; | ||
| const char *hostName = NULL; |
There was a problem hiding this comment.
⚪ [Info] Pointer declaration style inconsistent with repo convention
The newly introduced declaration uses const char *hostName = NULL; (asterisk bound to the name), whereas the surrounding wolfSSL code and the original removed line used const char* hostName (asterisk bound to the type). Minor consistency nit only.
Fix: Match the const char* name pointer style used elsewhere in the file.
| static int TLSX_EchChangeSNI(WOLFSSL* ssl, TLSX** pEchX, | ||
| char* serverName, TLSX** pServerNameX, | ||
| TLSX*** pExtensions) | ||
| /* Returns 1 if the extensions should be hidden for this write */ |
There was a problem hiding this comment.
⚪ [Info] TLSX_EchShouldHideInner can be a single boolean expression
The helper is a two-branch return of a simple predicate. It can be collapsed to a single expression for readability without changing behavior. Purely cosmetic.
Fix: Optionally simplify to a single return; take it or leave it.
There was a problem hiding this comment.
updated
- *_wire_sni test is now more efficient - openssl-ech workflow now does interop with ECH rejection extra improvements: - tested TLSX_EchSwapExtensions - added ctx level SNI to padding calculation
61e1638 to
4eef82d
Compare
Description
Reworks ECH public-vs-private extension handling. The old path swapped a single SNI string in place on the live extension list - this was fragile and SNI-only. This PR generalizes it to a public-extension list on the ECH struct that's swapped by extension type, so the outer ClientHello can carry arbitrary public extensions.
ZD#21931
Public extension manager
TLSX* extensionstoWOLFSSL_ECHholding the public extensions (e.g. the public-name SNI), populated inTLSX_ECH_UsefromechConfig->publicName.TLSX_EchChangeSNI/TLSX_EchRestoreSNIwith:TLSX_EchShouldHideInner- true when ECH is the outer type.TLSX_EchSwapExtensions- swaps matching extension types betweenssl->extensionsandech->extensions; unmatched public exts are prepended;popCountreverses leading nodes to preserve OuterExtensions ordering.TLSX_EchReplaceExtensions- accept: free the public list; reject: swap public exts intossl->extensions.TLSX_GetSizeWithEch/TLSX_WriteWithEchinstall the public exts before size/write and swap back after.ForceZerothe hpke; freeech->extensionsinTLSX_ECH_Free.SNI changes
EchStateSNIenum and thesniState/privateNamefields, plus the sniState transitions inDoTls13HandShakeMsgType.TLSX_SNI_Parse: drop the sniState-based private-name save/restore. On outer-CH parse, match against anyechConfig->publicNameand write a matched public SNI toech->extensions, notssl->extensions; simplify thematched/cacheOnlyand response-flag logic.TLSX_EchChangeSNI.SendTls13ClientHello: read padding length viaTLSX_SNI_GetRequestinstead ofech->privateName.TLSX_EchReplaceExtensionsat the accept/reject points (EchCheckAcceptance,DoTls13ServerHelloreject fallback,DoTls13ClientHello). This installs the correct SNI for use later in the handshake.Testing
test_wolfSSL_Tls13_ECH_wire_sni: drive the handshake manually and assert the public name is present and the private name absent in raw CH1/CH2 bytes, across accept/reject.test_wolfSSL_Tls13_ECH_no_private_name: no-inner-SNI now succeeds against a permissive server (ACCEPTED); rejected only withABORT_ON_ABSENCE.b64MandatoryFirstconfig case;test_ech_server_sni_callbackrejects a wrong public name; add a double-public-SNI scenario tobad_configs_ex.test_wolfSSL_Tls13_ECH_long_SNI(its overflow target - the deleted swap helpers - no longer exist) and relocate the over-long-SNIBAD_LENGTH_Echeck intotest_wolfSSL_UseSNI_params.Added testing from #10542:
Follow-ups
Checklist